<?php

require_once 'd:\WebServers\home\yii.my\htdocs\app\components\CLI.php';

/**
 * EMessageCommand class file. Enhanced from MessageCommand.
 *
 * Requires Yii >= 1.1.5
 *
 * @package	system.cli.commands
 * @link	http://www.yiiframework.com/extension/pophpcommand/
 * @author	Olivier M <eliovir@nospam.gmail.com>
 * @copyright	Copyright &copy; 2009 Eliovir
 * @license	http://www.yiiframework.com/license/
 * @since	2009-05-25
 * @version	2012-06-05
 */

/**
 * Sorting function
 *
 * @param	mixed	$a	first element (numeric or string)
 * @param	mixed	$b	second element (numeric or string)
 * @return	bool	sort direction
 * @version	2011-12-14
 */
function cmp_knatcasesort($a, $b) {
	if (is_numeric($a) && is_numeric($b)) {
		return $a > $b;
	} else {
		$icase = strcasecmp($a, $b);
		return $icase !== 0 ? $icase : strcmp($a, $b);
	}
}

/**
 * EMessageCommand converts translated messages from PHP message source files
 *
 * To gettext PO files, and vice. It also shows statistics for translations.
 *
 * @uses	CLI
 * @uses	CConsoleCommand
 * @package	system.cli.commands
 * @author	Olivier M <eliovir@nospam.gmail.com>
 * @copyright	Copyright &copy; 2009 Eliovir
 * @license	http://www.yiiframework.com/license/
 * @since	2009-05-25
 * @version	2012-06-05
 */
class EMessageCommand extends CConsoleCommand {
	const PHPHEADER = "<?php
/**
 * Message translations.
 *
 * This file is automatically generated by 'yiic emessage' command.
 * It contains the localizable messages extracted from source code.
 * You may modify this file by translating the extracted messages.
 *
 * Each array element represents the translation (value) of a message (key).
 * If the value is empty, the message is considered as not translated.
 * Messages that no longer need translation will have their translations
 * enclosed between a pair of '@@' marks.
 *
 * Message string can be used with plural forms format. Check i18n section
 * of the guide for details.
 *
 * NOTE, this file must be saved in UTF-8 encoding.
 *
 * @version \$Id: \$
 */
return ";
	const POHEADER = '# translation of %1$s to %2$s
# Copyright (C) %3$s
# This file is distributed under the same license as the %1s package.
#
# FULL NAME <EMAIL@ADDRESS>, %3$s.
msgid ""
msgstr ""
"Project-Id-Version: %1$s\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: %4$s\n"
"PO-Revision-Date: %4$s\n"
"Last-Translator: \n"
"Language-Team: %2$s <translation-team-%2$s@lists.sourceforge.net>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Yii EMessage\n"
"Generated-By: Yii EMessage\n"
"Plural-Forms: nplurals = 2; plural = n != 1;\n"

';
	const POTHEADER = '# translation of %1$s to LANGUAGE
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: %1$s\n"
"POT-Creation-Date: %2$s\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Yii EMessage\n"
"Generated-By: Yii EMessage\n"

';

	/**
	 * Config array from app/message/config.php
	 */
	protected static $config = array();

	/**
	 * @var	array	Files for the module - category - language
	 */
	private $_files = array();

	/**
	 * Provides the command description.
	 *
	 * @return	string	the command description.
	 */
	public function getHelp() {
		$cmd = $this->getCommandRunner()->getScriptName() . ' ' . $this->getName();
		return <<<EOD
USAGE
	$cmd <message|po|php|check|credits|duplicates|removeEmptyFiles|statistics> [options]

DESCRIPTION
	This command:
	- searches for messages to be translated in the specified source files
	  and compiles them into PHP arrays as message source;
	- converts messages from .php files to gettext .po files and vice;
	- checks the evaluation expression in the plural message. A misformed
	  expression can raise a 500 error, depending on php.ini.
	- shows the translation credits;
	- finds duplicates between all .php files;
	- removes the .php files where there is not any translation;
	- shows the translation statistics.

ACTIONS
	- check [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Check the plural expressions.
	- credits [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Shows the translation credits.
	- duplicates [--caseSensitive=true] [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Finds the duplicates between all .php files. The search can be case
	  insensitive. Run it after a 'message' execution, as it searches into the
	  first language.
	- message [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Searches for messages to be translated in the specified source files
	- po [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Converts messages from gettext .po files to .php files.
	- php [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Converts messages from .php files to gettext .po files.
	- removeEmptyFiles [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Remove the .php files where there is not any translated string.
	- statistics [--config=d:\WebServers\home\yii.my\htdocs\app/messages/config.php]
	  Shows the translation statistics.

CONFIGURATION FILE
	Provided by the argument --config=.
	By default, it is `d:\WebServers\home\yii.my\htdocs\app/messages/config.php`. The file must be a valid
	PHP script which returns an array of name-value pairs.
	Each name-value pair represents a configuration option. The following
	options must be specified:
	- messagePath: string, root directory containing message translations.
	- languages: array, list of language codes that the extracted messages
	  should be translated to. For example, array('zh_cn', 'en_au').
        - autoMerge: boolean, overwrite the .php files with the new extracted
          messages. Default: false.
	- launchpad: boolean, if the .po files must be stored as
	  app/messages/launchpad/template/lang.po or in the same directory
	  of the converted .php file. Default: false.
	- skipUnused: boolean, do not mark unused string with '@@' and skip
	  them. Default: false.
	- fileTypes: array, a list of file extensions (e.g. 'php', 'xml').
	  Only the files whose extension name can be found in this list
	  will be processed. If empty, all files will be processed.
	- exclude: array, a list of directory and file exclusions. Each
	  exclusion can be either a name or a path. If a file or directory name
	  or path matches the exclusion, it will not be copied. For example,
	  an exclusion of '.svn' will exclude all files and directories whose
	  name is '.svn'. And an exclusion of '/a/b' will exclude file or
	  directory 'sourcePath/a/b'.
	- translator: the name of the function for translating messages.
	  Defaults to 'Yii::t'. This is used as a mark to find messages to be
	  translated.

EOD;
	}

	/**
	 * Get a value from the configuration.
	 *
	 * @param	string	$key configuration key
	 * @return	mixed	value for the key
	 */
	private function config($key) {
		if (!isset(self::$config[$key])) {
			$this->usageError("The configuration file must contain the key $key.");
		}
		return self::$config[$key];
	}

	/**
	 * Store the configuration from a file.
	 *
	 * @param	string	$filename the file path for the configuration
	 */
	private function setConfig($filename) {
		if (!file_exists($filename)) {
			$this->usageError("The configuration file {$filename} does not exist.");
		}
		self::$config = require_once($filename);

		/*
		 * Checks
		 */
		if (!isset(self::$config['sourcePath'], self::$config['messagePath'], self::$config['languages'])) {
			$this->usageError('The configuration file must specify "sourcePath", "messagePath" and "languages".');
		}
		if (!is_dir(self::$config['sourcePath'])) {
			$this->usageError('The source path ' . self::$config['sourcePath'] . ' is not a valid directory.');
		}
		if (!is_dir(self::$config['messagePath'])) {
			$this->usageError('The message path ' . self::$config['messagePath'] . ' is not a valid directory.');
		}
		if (empty(self::$config['languages'])) {
			$this->usageError("Languages cannot be empty.");
		}
	}

	/**
	 * Echo the names of contributors for all translations.
	 *
	 * @param	string	$config the file path for the translation config
	 */
	public function actionCheck($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		$origfiles = $this->phpFiles();
		foreach ($origfiles as $file) {
			$msgs = include($file);
			foreach ($msgs as $msg) {
				if (!self::checkSyntax($msg)) {
					echo "In $file, the plural expression is wrong for \"$msg\".\n";
				}
			}
		}
	}

	/**
	 * Echo the names of contributors for all translations.
	 *
	 * @param	string	$config the file path for the translation config
	 */
	public function actionCredits($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);

		$origfiles = $this->phpFiles();

		// get the translator-credits strings
		$credits = array();
		foreach ($origfiles as $file) {
			$msgs = include($file);
			if (!isset($msgs['translator-credits'])) {
				continue;
			}
			$names = explode("\n", $msgs['translator-credits']);
			foreach ($names as $name) {
				if ($name === 'Launchpad Contributions:' || $name === '') {
					continue;
				}
				$name = explode(' ', trim($name));
				array_pop($name);
				$name = join(' ', $name);
				if (!in_array($name, $credits)) {
					$credits[] = $name;
				}
			}
		}
		sort($credits);
		echo '* ' . join("\n* ", $credits);
	}

	/**
	 * Find duplicates betweed all .php files.
	 *
	 * @param	string	$caseSensitive
	 * @param	string	$config the file path for the translation config
	 */
	public function actionDuplicates($caseSensitive='true', $config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		$languages = $this->config('languages');
		$language = $languages[0];
		$messagePath = $this->config('messagePath');
		$this->duplicates($language, $messagePath, $caseSensitive);
	}

	public function actionHelp() {
		echo $this->getHelp();
	}

	public function actionIndex() {
		echo $this->getHelp();
	}

	public function actionMessage($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		return self::message();
	}

	public function actionPo($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		$this->convert('po');
	}

	public function actionPhp($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		$this->convert('php');
	}

	public function actionRemoveEmptyFiles($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		$origfiles = array_merge($this->phpFiles(), $this->poFiles());
		foreach ($origfiles as $file) {
			if (substr($file, -4) == '.php') {
				$translations = include($file);
			} else if (substr($file, -3) == '.po') {
				$translations = $this->loadPO($file);
			}
			if (!self::isTranslated($translations)) {
				echo "Removing $file" . PHP_EOL;
				unlink($file);
				@rmdir(dirname($file));
			}
		}
	}

	public function actionStatistics($config='d:\WebServers\home\yii.my\htdocs\app/messages/config.php') {
		$this->setConfig($config);
		$languages = $this->config('languages');
		$sourcePath = $this->config('sourcePath');
		return self::statistics($languages, $sourcePath);
	}

	/**
	 * Check the plural expressions in a translated string.
	 */
	public static function checkSyntax($msg) {
		$matches = array();
		$n = preg_match_all('/\s*([^#]*)\s*#([^\|]*)\|/', $msg . '|', $matches);
		if ($n <= 1) {
			return true;
		}
		for ($i = 0; $i < $n; ++$i) {
			$expression = $matches[1][$i];
			if (is_numeric($expression)) {
				return true;
			}
			$res = @eval(str_replace('n', '$n', $expression) . '; return true;');
			if ($res === false) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Convert the gettext files to .php file and vice.
	 *
	 * @param	string	$ext source file extension (po or php)
	 */
	public function convert($ext) {
		$year = date('Y');
		$date = date('Y-m-d H:i+0000');
		$launchpad = $this->config('launchpad');
		$messagePath = $this->config('messagePath');

		if ($launchpad && !file_exists("$messagePath/launchpad")) {
			mkdir("$messagePath/launchpad");
		}

		$options = array(
			'fileTypes'=>array($ext),
		);
		if ($ext != 'po' || !$launchpad) {
			$options['exclude'] = array('launchpad');
		}
		$messagePath = realpath($messagePath);
		$origfiles = CFileHelper::findFiles($messagePath, $options);
		$origfiles = array_merge($origfiles, glob(dirname($this->config('sourcePath')) . '/modules/*/messages/*/*.' . $ext));
		if (empty($origfiles)) {
			$this->fatalError('No files to convert where found in neither ' . $messagePath . ' nor ' . Yii::app()->basePath . '/modules/*/messages/*/*.' . $ext);
			return;
		}
		foreach ($origfiles as $file) {
			$file = realpath($file);
			$language = basename(dirname($file));
			// Convert PHP files to PO
			if ($ext == 'php') {
				if (basename($file) == 'config.php') {
					continue;
				}
				$destfile = str_replace('.php', '.po', $file);
				if ($launchpad) {
					$template = str_replace('.php', '', basename($file));
					$language = basename(dirname($file));
					// template = nameModule.template if module
					$parts = explode('/', $file);
					if (in_array('modules', $parts)) {
						$template = $parts[array_search('modules', $parts) + 1] . '.' . $template;
					}
					unset($parts);
					// -
					$destdir = "$messagePath/launchpad/$template/";
					if (!file_exists($destdir)) {
						mkdir($destdir);
					}
					$destfile = "$destdir/$language.po";
				}
				$messages = include($file);
				$header = sprintf(self::POHEADER, basename($file), $language, $year, $date);
				self::savePO($destfile, $messages, $header);
				// Generate .pot
				if ($launchpad) {
					$destfile = "$messagePath/launchpad/$template/$template.pot";
					$msgids = array_keys($messages);
					$messages = array_fill_keys($msgids, '');
					$header = sprintf(self::POTHEADER, basename($file), $date);
					self::savePO($destfile, $messages, $header);
				}
			// Convert PO files to PHP
			} else {
				$destfile = str_replace('.po', '.php', $file);
				if ($launchpad) {
					$language = str_replace(".$ext", '', basename($file));
					$template = basename(dirname($file));
					if (($pos = strpos($template, '.')) !== false) {
						$moduleClass = substr($template, 0, $pos);
						$moduleCategory = substr($template, $pos + 1);
						$template = $moduleClass . 'Module.' . $moduleCategory;
					}
					$destfile = $this->getPhpMessageFile($template, $language);
				}
				$array = self::loadPO($file, '');
				uksort($array, 'cmp_knatcasesort');
				$array = str_replace("\r", '', var_export($array, true));
				file_put_contents($destfile, self::PHPHEADER . $array . ';' . PHP_EOL);
			}
			echo str_replace($this->config('sourcePath') . '/', '', $file) . '=>' . ($ext == 'php' ? 'po' : 'php') . "\n";
		}
	}

	/**
	 * Loads messages from a PO file.
	 *
	 * @param	string	filepath
	 * @return	array	message translations (source message=>translated message)
	 */
	protected static function loadPO($file) {
		$messages = array();

		// match all msgid/msgstr entries
		$pattern = '/(msgid\s+("(.*|\\\\")*?"\s*)+)\s+' .
			'(msgstr\s+("(.*|\\\\")*?"\s*)+)/';
		$content = '';
		$lines = file($file);
		foreach ($lines as $line) {
			if (substr($line, 0, 2) == '#~') {
				continue;
			}
			$content .= $line;
		}
		$matched = preg_match_all($pattern, $content, $matches);
		unset($content);
		if (!$matched) {
			return $messages;
		}

		// get all msgids and msgtrs
		for ($i = 0; $i < $matched; $i++) {
			$msgid = preg_replace('/\s*msgid\s*"(.*)"\s*/s', '\\1', $matches[1][$i]);
			$msgstr = preg_replace('/\s*msgstr\s*"(.*)"\s*/s', '\\1', $matches[4][$i]);
			$msgid = self::decode($msgid);
			$msgstr = self::decode($msgstr);
			if ($msgid == '') {
				continue;
			}
			$messages[$msgid] = $msgstr;
		}
		return $messages;
	}

	/**
	 * List all PHP translation files.
	 *
	 * @return	array	full paths for the PHP translation files
	 */
	protected function phpFiles() {
		// retrieve all categories
		$messagePath = $this->config('messagePath');
		$messagePath = realpath($messagePath);
		$options = array(
			'fileTypes'=>array('php'),
		);
		$origfiles = CFileHelper::findFiles($messagePath, $options);
		$origfiles = array_merge($origfiles, glob(dirname($this->config('sourcePath')) . '/modules/*/messages/*/*.php'));
		foreach ($origfiles as $n=>$file) {
			$origfiles[$n] = realpath($file);
		}
		return $origfiles;
	}

	/**
	 * List all PO translation files.
	 *
	 * @return	array	full paths for the gettext translation files
	 */
	protected function poFiles() {
		$messagePath = $this->config('messagePath');
		$messagePath = realpath($messagePath);
		$origfiles = glob($messagePath . '/launchpad/*/*.po');
		$origfiles = array_merge($origfiles, glob(dirname($this->config('sourcePath')) . '/modules/*/messages/*/*.po'));
		return $origfiles;
	}

	/**
	 * Saves messages to a PO file.
	 *
	 * Note if the message has a context, the message id must be prefixed with
	 * the context with chr(4) as the separator.
	 *
	 * @param	string	filepath
	 * @param	array	message translations (message id=>translated message).
	 * @param	string	header for the PO file
	 */
	protected static function savePO($file, $messages, $header='') {
		$content = $header;
		foreach ($messages as $id=>$message) {
			if ($id == 'translator-credits') {
				$content .= "#. Put one translator per line, in the form of NAME <URL>.\n";
			}
			if (($pos = strpos($id, chr(4))) !== false) {
				$content .= 'msgctxt "' . substr($id, 0, $pos) . "\"\n";
				$id = substr($id, $pos+1);
			}
			$content .= 'msgid "' . self::encode($id) . "\"\n";
			$content .= 'msgstr "' . self::encode($message) . "\"\n\n";
		}
		file_put_contents($file, $content);
	}

	/**
	 * Encodes special characters in a message.
	 *
	 * @param	string	message to be encoded
	 * @return	string	the encoded message
	 */
	public static function encode($string, $beginning='msgstr') {
		$encoded = str_replace(array('\\', '"', "\n", "\t", "\r"), array('\\\\', '\\"', "\\n", '\\t', '\\r'), $string);
		$length = strlen($encoded);
		//$length = mb_strlen($encoded);
		$max_length = 77;
		$text = $length + strlen($beginning) >= $max_length && $length < $max_length ? "\"\n\"" : '';
		$word = '';
		$pos = 0;
		$prev1 = null;
		for ($i = 0; $i<$length; $i++) {
			$letter = substr($encoded, $i, 1);
			//$letter = mb_substr($encoded, $i, 1);
			if (
				$i - $pos == $max_length || ($prev1 == '\\' &&  $letter == 'n')) {
				if ($prev1 == '\\' && $letter == 'n') {
					$text .= $word . $letter;
					$letter = '';
					$word = '';
				}
				$text .= "\"\n\"";
				$pos = $i;
			}
			$word .= $letter;
			if ($letter == ' ' || $letter == "\t" || $letter == '-') {
				$text .= $word;
				$word = '';
			}
			$prev1 = $letter;
		}
		if ($pos != 0) {
			$text = "\"\n\"" . $text;
		}
		return $text . $word;
	}

	/**
	 * Decodes special characters in a message.
	 *
	 * @param	string	message to be decoded
	 * @return	string	the decoded message
	 */
	public static function decode($string) {
		return str_replace(array("\"\n\"", '\\\\"', '\\"', "\\n", '\\t', '\\r'), array('', '\\"', '"', "\n", "\t", "\r"), $string);
	}

	/**
	 * Find duplicates in all .php files.
	 *
	 * @param	string	$language the target language
	 * @param	string	$messagePath root directory containing message translations for the application.
	 * @param	boolean	$caseSensitive the search is case sensitive
	 */
	protected function duplicates($language, $messagePath, $caseSensitive) {
		$msgs = array();
		$duplicates = array();
		$messagePath = realpath($messagePath . '/' . $language);
		$options = array(
			'fileTypes'=>array('php'),
		);
		$origfiles = CFileHelper::findFiles($messagePath, $options);
		$origfiles = array_merge($origfiles, glob(dirname($this->config('sourcePath')) . '/modules/*/messages/' . $language . '/*.php'));
		foreach ($origfiles as $origfile) {
			$msgs[$origfile] = array_keys(include($origfile));
			if (in_array('translator-credits', $msgs[$origfile])) {
				unset($msgs[$origfile][array_search('translator-credits', $msgs[$origfile])]);
			}
			if ($caseSensitive == false) {
				foreach ($msgs[$origfile] as $n=>$msgid) {
					$msgs[$origfile][$n] = strtolower($msgid);
				}
			}
		}

		foreach ($origfiles as $origfile) {
			foreach ($msgs[$origfile] as $msg) {
				foreach ($msgs as $file=>$msgids) {
					if ($file === $origfile) {
						continue;
					}
					if (in_array($msg, $msgids)) {
						if (!isset($duplicates[$msg])) {
							$duplicates[$msg] = array($origfile);
						}
						if (!in_array($file, $duplicates[$msg])) {
							$duplicates[$msg][] = $file;
						}
					}
				}
			}
		}
		// display
		foreach ($duplicates as $msg=>$files) {
			var_export($msg);
			echo PHP_EOL;
			foreach ($files as $file) {
				echo str_repeat(' ', 10) . realpath($file) . PHP_EOL;
			}
		}

	}

	/**
	 * Message command
	 */
	protected function message() {
		echo CLI::ansicolor('Searching message to be translated.', 'CYAN') . PHP_EOL;
		$translator = 'Yii::t';
		// default values
		$sourcePath = '.';
		$languages = array();
		$messagePath = 'app/messages';
		//
		extract(self::$config);
		$options = array(
			'fileTypes'=>$this->config('fileTypes'),
			'exclude'=>$this->config('exclude'),
		);
		$files = CFileHelper::findFiles(realpath($sourcePath), $options);

		$messages = array();
		foreach ($files as $file)
			$messages = array_merge_recursive($messages, $this->extractMessages($file, $translator));

		echo CLI::ansicolor('Compiling message sources.', 'CYAN') . PHP_EOL;
		foreach ($languages as $language) {
			$dir = $messagePath . DIRECTORY_SEPARATOR . $language;
			if (!is_dir($dir))
				@mkdir($dir);
			foreach ($messages as $category=>$msgs) {
				$msgs = array_values(array_unique($msgs));
				$file = $this->getPhpMessageFile($category, $language);
				$this->generateMessageFile($msgs, $file);
			}
		}
		echo CLI::ansicolor('Message source compiled.', 'CYAN') . PHP_EOL;
	}

	/**
	 * Extracts the messages from the PHP source codes.
	 *
	 * @access	protected
	 * @param	string	file path
	 * @param	string	translator function, eg. 'Yii::t'
	 * @return	array	messages to translate, by category (category=>(messages))
	 */
	protected function extractMessages($fileName, $translator) {
		echo 'Extracting messages from ' . str_replace(dirname($this->config('sourcePath')) . DIRECTORY_SEPARATOR, '', realpath($fileName)) . "...\n"; // more readable
		$subject = file_get_contents($fileName);
		$n = preg_match_all('/\b' . $translator . '\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s', $subject, $matches, PREG_SET_ORDER);
		$messages = array();
		for ($i = 0; $i<$n; ++$i) {
			$category = substr($matches[$i][1], 1, -1);
			if ($category == 'yii') {
				continue;
			}
			$message = $matches[$i][2];
			$messages[$category][] = eval("return $message;"); // use eval to eliminate quote escape
			if ($messages[$category][count($messages[$category]) -1] == '0')
				unset($messages[$category][count($messages[$category]) -1]);
		}
		// extract __('msgid')
		$translator = '__';
		$n = preg_match_all('/\b' . $translator . '\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s', $subject, $matches, PREG_SET_ORDER);
		$category = 'main';
		for ($i = 0; $i<$n; ++$i) {
			$message = $matches[$i][1];
			$messages[$category][] = eval("return $message;"); // use eval to eliminate quote escape
		}
		//-
		return $messages;
	}

	/**
	 * Saves the messages in a PHP file.
	 *
	 * @access	protected
	 * @param	array	messages and translations (message id => translated message)
	 * @param	string	file path where to translate
	 * @return	void
	 */
	protected function generateMessageFile($messages, $fileName) {
		echo 'Saving messages to ' . str_replace(dirname($this->config('sourcePath')) . DIRECTORY_SEPARATOR, '', realpath($fileName)) . '... '; // more readable
		if (is_file($fileName)) {
			$translated = require($fileName);
			sort($messages);
			ksort($translated);
			if (array_keys($translated) == $messages) {
				echo "nothing new... skipped.\n";
				return;
			}
			$merged = array();
			$untranslated = array();
			foreach ($messages as $message) {
				if (!empty($translated[$message]))
					$merged[$message] = $translated[$message];
				else
					$untranslated[] = $message;
			}
			ksort($merged);
			sort($untranslated);
			$todo = array();
			foreach ($untranslated as $message)
				$todo[$message] = '';
			ksort($translated);
			if (!isset(self::$config['skipUnused']) || self::$config['skipUnused'] = false) {
				foreach ($translated as $message=>$translation) {
					if (!isset($merged[$message]) && !isset($todo[$message])) {
						$todo[$message] = '@@' . $translation . '@@';
					}
				}
			}
			$merged = array_merge($todo, $merged);
			if (!isset(self::$config['autoMerge']) || self::$config['autoMerge'] = false) {
				$fileName .= '.merged';
			}
			echo "translation merged.\n";
		} else {
			$merged = array();
			foreach ($messages as $message)
				$merged[$message] = '';
			echo "saved.\n";
		}
		uksort($merged, 'cmp_knatcasesort');
		$array = str_replace("\r", '', var_export($merged, true));
		$content = self::PHPHEADER . "$array;" . PHP_EOL;
		file_put_contents($fileName, $content);
	}

	/**
	 * Determines the PHP message file name based on the given category and language.
	 *
	 * If the category name contains a dot, it will be split into the module class name and the category name.
	 * In this case, the message file will be assumed to be located within the 'messages' subdirectory of
	 * the directory containing the module class file.
	 * Otherwise, the message file is assumed to be under the {@link CPhpMessageSource->basePath}.
	 *
	 * @see	CPhpMessageSource::getMessageFile()
	 * @param	string	$category category name
	 * @param	string	$language language ID
	 * @return	string	the message file path
	 */
	protected function getPhpMessageFile($category, $language) {
		if (!isset($this->_files[$category][$language])) {
			if (($pos = strpos($category, '.')) !== false) {
				$moduleClass = substr($category, 0, $pos);
				$moduleCategory = substr($category, $pos + 1);
				if (!class_exists($moduleClass, false)) {
					Yii::app()->getModule(strtolower(substr($moduleClass, 0, -6)));
				}
				$class = new ReflectionClass($moduleClass);
				$this->_files[$category][$language] = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'messages' . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $moduleCategory . '.php';
			} else {
				$this->_files[$category][$language] = self::$config['messagePath'] . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php';
			}
			$dir = dirname($this->_files[$category][$language]);
			if (!file_exists($dir)) {
				if (strlen($dir) > 255) {
					echo $dir;
				}
				mkdir($dir, 0777, true);
			}
		}
		return $this->_files[$category][$language];
	}

	/**
	 * Detect if an array of translations contains translated sentences.
	 *
	 * @param	array	$translations array of translations from a message PHP file.
	 * @return	boolean	existence of translated sentences in the array
	 */
	public static function isTranslated($translations) {
		foreach ($translations as $msgstr) {
			if ($msgstr !== '') {
				return true;
			}
		}
		return false;
	}

	/**
	 * Statistics command
	 *
	 * @access	protected
	 * @static
	 * @param	mixed	$languages
	 * @param	string	$sourcePath the directory path containing app/
	 * @return	void
	 */
	protected static function statistics($languages, $sourcePath) {
		/*
		 * Get max pad for each column
		 */
		$pads = array(
			'language'=>0,
			'file'=>0,
			'untranslated'=>0,
			'total'=>0,
		);
		$stats = array();
		foreach ($pads as $name=>$v) {
			$pads[$name] = strlen($name);
		}
		foreach ($languages as $language) {
			if ($pads['language'] < strlen($language)) {
				$pads['language'] = strlen($language);
			}
			$stat = self::stat($language, $sourcePath);
			$stats[$language] = $stat;
			foreach ($stat['pads'] as $k=>$v) {
				if ($pads[$k] < $v) {
					$pads[$k] = $v;
				}
			}
		}
		$pads_sum = 0;
		foreach ($pads as $name=>$pad) {
			$pads[$name]++;
			$pads_sum += $pads[$name];
		}

		/*
		 * Header
		 */
		echo CLI::hline($pads_sum);
		foreach ($pads as $name=>$pad) {
			echo str_pad($name, $pad, ' ', STR_PAD_RIGHT);
		}
		echo PHP_EOL;
		echo CLI::hline($pads_sum);

		/*
		 * Body
		 */
		foreach ($languages as $language) {
			$stat = $stats[$language];
			if ($stat['total'] == 0) {
				$ratio = 0;
			} else {
				$ratio = 1 - ($stat['untranslated']/$stat['total']);
			}
			echo self::colorize(str_pad($language, $pads['language'], ' ', STR_PAD_RIGHT) .
				str_repeat(' ', $pads['file']) .
				str_pad($stat['untranslated'], $pads['untranslated'], ' ', STR_PAD_RIGHT) .
				str_pad($stat['total'], $pads['total'], ' ', STR_PAD_RIGHT), $ratio) .
				"\n";
			foreach ($stat['details'] as $f=>$s) {
				if ($s['total'] == 0) {
					$ratio = 0;
				} else {
					$ratio = 1 - ($s['untranslated'] / $s['total']);
				}
				echo str_repeat(' ', $pads['language']) .
					self::colorize(str_pad($f, $pads['file'], ' ', STR_PAD_RIGHT) .
					str_pad($s['untranslated'], $pads['untranslated'], ' ', STR_PAD_RIGHT) .
					str_pad($s['total'], $pads['total'], ' ', STR_PAD_RIGHT), $ratio) .
					"\n";
			}
			echo CLI::hline($pads_sum);
		}

		/*
		 * Footer
		 */
		echo self::colorize('0%', 0) . ' ' .
			self::colorize('>0%', 0.01) . ' ' .
			self::colorize('25%', 0.25) . ' ' .
			self::colorize('50%', 0.50) . ' ' .
			self::colorize('75%', 0.75) . ' ' .
			self::colorize('100%', 1) . PHP_EOL;
	}

	/**
	 * Statistics for on language
	 *
	 * @param	string	$language the language to study
	 * @return	array	details ('untranslated'=>, 'total'=>, 'details'=>(filename=>('untranslated'=>, 'total'=>))
	 */
	protected function stat($language, $sourcePath) {
		$stat = array('untranslated'=>0, 'total'=>0, 'details'=>array());
		$pads = array('untranslated'=>0, 'total'=>0, 'file'=>0);
		$d = self::$config['messagePath'] . '/' . $language;
		$files = glob($d . '/*php');
		$files = array_merge($files, glob(dirname($sourcePath) . '/modules/*/messages/' . $language . '/*php'));
		foreach ($files as $f) {
			$f = realpath($f);
			$t = include($f);
			$c = array_count_values($t);
			if (!isset($c['']))
				$c[''] = 0;
			$stat['untranslated'] += $c[''];
			$stat['total'] += count($t);
			$d = basename(dirname(dirname(dirname($f))));
			if ($d == basename($sourcePath)) {
				$d = '';
			} else {
				$d .= '/';
			}
			$display = $d . basename($f);
			$stat['details'][$display] = array('untranslated'=>$c[''], 'total'=>count($t));
			/*
			 * Max pad
			 */
			if ($pads['file'] < strlen($display)) {
				$pads['file'] = strlen($display);
			}
		}
		$pads['untranslated'] = strlen($stat['untranslated']);
		$pads['total'] = strlen($stat['total']);
		$stat['pads'] = $pads;
		return $stat;
	}

	/**
	 * Returns a colorized string for the shell according to ratio.
	 *
	 * @param	string	$message
	 * @param	float	$ratio
	 * @return	string	colorized string
	 * @uses	CLI::ansicolor()
	 */
	public static function colorize($message, $ratio) {
		if ($ratio == 1) {
			return CLI::ansicolor($message, 'GREEN');
		} elseif ($ratio >= 0.75) {
			return CLI::ansicolor($message, 'CYAN');
		} elseif ($ratio >= 0.5) {
			return CLI::ansicolor($message, 'YELLOW');
		} elseif ($ratio >= 0.25) {
			return CLI::ansicolor($message, 'WHITE');
		} elseif ($ratio > 0) {
			return CLI::ansicolor($message, 'MAGENTA');
		} elseif ($ratio == 0) {
			return CLI::ansicolor($message, 'RED');
		}
	}
	
	/**
	 * Display an error an exit with code 1.
	 *
	 * @param	string	$message	message error
	 */
	protected function fatalError($message) {
		echo "Error: $message\n\n";
		exit(1);
	}
}
